package org.mte.sak.file;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;


/**
 * <p>Crea un file compresso in formato <i>zip</i>.
 * Il file compresso viene salvato in un percorso specificato o
 * restituito in formato binario.</p>
 *
 * <p>È possibile specificare i percorsi delle risorse di un <i>file system</i>
 * che si vogliono comprimere e il percorso del completo del file zip che si
 * vuole ottenere.
 * (vedi metodo <code>makeZip(String[] paths, String targetFilePath)</code>.</p>
 *
 * <p>Alternativamente si può definire un'oggetto <code>HashMap</code>
 * contenente o percorsi completi di risorse su <i>file system</i> o oggetti
 * di tipo <code>StringBuffer</code> e ottenere un flusso binario rappresentate
 * l'equivalente del file zip contenente i dati passati in input compressi.
 * (vedi metodo <code>makeZip(HashMap data)</code>).</p>
 *
 * @author Marco Traversari
 * @version $Revision: 1.1 $ $Date: 2011-04-18 08:37:59 $
 */
public class Zip {

	/**
	 * Flusso di dati compressi.
	 */
	protected ZipOutputStream zos = null;

	/**
	 * Dimensione del blocco di byte con il quale
	 * vengono letti i file.
	 */
	protected final int DATA_BLOCK_SIZE = 2048;

	/**
	 * � il separatore delle risorse del <code>file system</code>
	 * di default vale come quello di sistema.
	 */
	protected String fileSeparator = System.getProperty("file.separator");

	/**
	 * Costuttore default.
	 */
	public Zip() {
	}

	/**
	 * Istanzia l'oggetto assegnando il separatore dei file.
	 */
	public Zip(String fileSeparator) {
		this.fileSeparator = fileSeparator;
	}

	/**
	 * Crea file compresso di una serie di file e directory.
	 *
	 * @param paths � l'array di percorsi in cui si trovano i file e directory
	 *				da comprimere inclusi gli stessi.
	 * @param targetFilePath � il percorso completo file  compresso
	 * 				che si vuole creare.
	 *
	 * @throws IOException se si verificano problemi di accesso al <i>file system</code>.
	 */
	public void makeZip(String[] paths, String targetFilePath) throws IOException {
		boolean isNewFile = (new File(targetFilePath)).createNewFile();
		if (isNewFile) {
			FileOutputStream fos = new FileOutputStream(targetFilePath);
			zos = new ZipOutputStream(fos);
			String pathToZip = null;
			String[] relativePaths = new String[paths.length];

			for (int  i = 0; i < paths.length; i++) {
				relativePaths[i] = getSubString(paths[i], fileSeparator, true);
				paths[i] = getSubString(paths[i], fileSeparator, false);
				File file = new File(paths[i], relativePaths[i]);
				pathToZip = file.getPath();

				if (file.isDirectory()) {
					addDirectory(paths[i], pathToZip);
				} else {
					addFile(paths[i], pathToZip);
				}
			}

			zos.flush();
			zos.close();
			fos.close();
		} else {
			throw new IOException(targetFilePath + " already exists.");
		}

	}

	/**
	 * <p>Crea un array di byte compressi nel formato zip partendo da un oggetto
	 * <code>HashMap</code> contenente il percorso dei file da comprimere
	 * o eventualmente di un oggetto <code>StringBuffer</code>
	 * con i dati da comprimere.</p>
	 * <p>
	 * Nell'oggetto <code>HashMap<code> le chiavi sono sempre delle stringhe
	 * e nel caso che il valore � del tipo <code>StringBuffer</code>
	 * rappresentano il nome del file in cui sar� salvato il gruppo
	 * di byte da comprimere.
	 * </p>
	 * <p>
	 * Es: key				value
	 * </p>
	 * <ul>
	 * <li><i>Grid.xml</i>			<i>StringBuffer:dati della griglia</i></li>
	 * <li><i>Grid.xsl</i>			<i>percorso del file Grid.xsl</i></li>
	 * </ul>
	 *
	 * @param data è la <code>HashMap</code> specificata.
	 *
	 * @return un <code>ByteArrayOutputStream</code> contenente il file compresso
	 * 			in formato binario.
	 *
	 * @throws IOException se si verificano problemi di accesso al <i>file system</code>.
	 */
	@SuppressWarnings("rawtypes")
	public ByteArrayOutputStream makeZip(HashMap data) throws IOException {
	 	ByteArrayOutputStream baos = new ByteArrayOutputStream();
	 	zos = new ZipOutputStream(baos);
		Iterator iterator = data.keySet().iterator();
		while (iterator.hasNext()) {
			String zipEntryName = (String) iterator.next();
			Object object = data.get(zipEntryName);
			if (object instanceof java.lang.String) {
				String path = (String) object;
				String relativePath = getSubString(path, fileSeparator, true);
				String parentPath = getSubString(path, fileSeparator, false);
				File file = new File(parentPath, relativePath);
				String pathToZip = file.getPath();
				if (file.isDirectory()) {
					addDirectory(parentPath, pathToZip);
				} else {
					addFile(parentPath, pathToZip);
				}
			} else if (object instanceof StringBuffer){
		  		zos.putNextEntry(new ZipEntry(zipEntryName));
				StringBuffer buffer = (StringBuffer) object;
				String strBuffer = buffer.toString();
				byte[] bytes = strBuffer.getBytes();
			    zos.write(bytes, 0, strBuffer.length());
			} else if (object instanceof StringBuilder) {
		  		zos.putNextEntry(new ZipEntry(zipEntryName));
				StringBuilder b = (StringBuilder) object;
				String str = b.toString();
				byte[] bytes = str.getBytes();
			    zos.write(bytes, 0, str.length());
			} else if (object instanceof ByteArrayOutputStream) {
				ByteArrayOutputStream bs = (ByteArrayOutputStream) object;
				zos.putNextEntry(new ZipEntry(zipEntryName));
				byte[] bytes = bs.toByteArray();
				zos.write(bytes, 0, bs.size());
			}
			zos.closeEntry();
		}
        zos.close();
	 	return baos;
	}

	/**
	 * Comprime il contentuto di un oggetto <code>FileEntity</code>
	 * e lo restituiesce in un <code>ByteArrayOutputStream</code>.
	 *
	 * @param fileObject
	 * @return un oggetto <code>ByteArrayOutputStream</code>
	 * @throws IOException
	 */
	public ByteArrayOutputStream makeZip(FileObject fileObject) throws IOException {
		ByteArrayOutputStream zbaos = null;
		if (fileObject != null && fileObject.getContent() != null) {
			zbaos = new ByteArrayOutputStream();
			zos = new ZipOutputStream(zbaos);
			zos.putNextEntry(new ZipEntry(fileObject.getFileName()));
			zos.write(fileObject.getContent().toByteArray());
			zos.closeEntry();
			zos.close();
		}
		return zbaos;
	}

	/**
	 * Aggiunge un file al flusso di dati da comprimere.
	 *
	 * @param path � la sezione del percorso del file che non
	 * 			viene inserita nel file compresso.
	 * @param pathToZip � la sezione del percorso del file che viene
	 * 			inserita nel file compresso, contiene naturalmente
	 * 			il nome del file.
	 *
	 * @throws IOException se si verificano problemi di accesso al <i>file system</code>.
	 *
	 */
	private void addFile(String path, String pathToZip) throws IOException {
		// Create a file input stream and a buffered input stream.
		FileInputStream fis = new FileInputStream(pathToZip);
		BufferedInputStream bis = new BufferedInputStream(fis);
		// Create a Zip Entry and put it into the archive (no data yet).
		ZipEntry fileEntry = new ZipEntry(pathToZip.substring(path.length()));
		zos.putNextEntry(fileEntry);

		// Create a byte array object named data and declare byte count variable.
		byte[] data = new byte[DATA_BLOCK_SIZE];
		int byteCount;

		// Create a loop that reads from the buffered input stream and writes
		// to the zip output stream until the bis has been entirely read.
		while ((byteCount = bis.read(data, 0, DATA_BLOCK_SIZE)) > -1) {
			zos.write(data, 0, byteCount);
		}
	}

	/**
	 * Percorre l'intero ramo del <i>file system</i> partendo dalla directory
	 * passata in input e ricorsivamente aggiunge al file zip i file che
	 * incontra.
	 *
	 * @param path � la sezione del percorso della directory che non
	 * 			viene inserita nel file compresso.
	 *
	 * @param dirPath � la sezione del percorso della directory che viene
	 * 			inserita nel file compresso.
	 *
	 * @throws IOException se si verificano problemi di accesso al <i>file system</code>.
	 */
	private void addDirectory(String path, String dirPath) throws IOException {
		File dir = new File(dirPath);
		if (dir.exists() == true) {
				//Create an array of File objects, one for each file or directory in dirObj.
				File[] fileList = dir.listFiles();

				//Loop through File array and display.
				for (int i = 0; i < fileList.length; i++){

					if (fileList[i].isDirectory()) {

						addDirectory(path, fileList[i].getPath());

					} else if (fileList[i].isFile()) {

						addFile(path, fileList[i].getPath());
					}
				}

		} else {
			throw new IOException("Directory " + dirPath + " does not exist.");
		}
	}

	/**
	 * Restituisce la parte a destra o sinistra di una stringa
	 * in base all'ultima occorrenza del carattere specificato.
	 *
	 * @param str stringa iniziale.
	 * @param character � il separatore.
	 * @param isRight indica se si vuole la parte destra,
	 * 			altrimenti viene restituita la parte sinistra.
	 *
	 * @return la sotto-stringa.
	 */
	private String getSubString(String str, String character, boolean isRight) {
		int index = str.lastIndexOf(character);
		String ext = "";
		if (isRight) {
			ext = (index != -1) ? str.substring(index + 1) : str;
		} else {
			ext = (index != -1) ? str.substring(0, index) : str;
		}
		return ext;
	}
}